#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2015 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
import re

from packagelib import *
from storagelib import *
from testlib import *


# vdo only exists on RHEL
SUPPORTED_OS = ["centos-8-stream", "rhel-8-4", "rhel-8-4-distropkg", "rhel-8-5", "rhel-8-6", "rhel-8-5-distropkg"]


@skipImage("no matching kmod-kvdo package yet on RHEL 9", "rhel-9-0", "rhel-9-0-distropkg")
class TestStorageVDO(StorageCase):

    def testVdo(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        # Make a volume group in which to create the VDO LV
        m.add_disk("10G", serial="DISK1")
        b.wait_in_text("#drives", "DISK1")
        m.execute("vgcreate vdo_vgroup /dev/sda")
        b.wait_in_text("#devices", "vdo_vgroup")

        b.click('.sidepanel-row:contains("vdo_vgroup")')
        b.wait_in_text("#detail-content", "No logical volumes")

        b.click("button:contains(Create new logical volume)")
        self.dialog_wait_open()
        b._wait_present("[data-field='purpose'] select option[value='block']")

        if m.image not in SUPPORTED_OS:
            b.wait_not_present("[data-field='purpose'] select option[value='vdo']")
            return

        # create VDO LV with default options and default virtual size
        self.dialog_set_val("name", "vdo0")
        self.dialog_set_val("purpose", "vdo")
        self.dialog_set_val("vdo_psize", 6 * 1024)
        self.dialog_apply()
        self.dialog_wait_close()

        # pool name gets auto-generated
        pool_name = "vpool0"

        self.content_row_wait_in_col(1, 1, "vdo0")
        # the pool does not appear as a top-level volume
        b.wait_not_in_text("#detail-content", pool_name)
        # Volume tab
        self.content_tab_wait_in_info(1, 1, "Name", "vdo0")
        self.content_tab_wait_in_info(1, 1, "Size", "10 GiB", "10.0 GiB")
        # VDO Pool tab
        self.content_tab_wait_in_info(1, 2, "Name", pool_name)
        self.content_tab_wait_in_info(1, 2, "Size", "6 GiB")
        # initial physical usage is 4 GiB, overhead for the deduplication index
        self.content_tab_wait_in_info(1, 2, "Data used", "4.00 GiB (67%)")
        self.content_tab_wait_in_info(1, 2, "Metadata used", "0%")
        b.wait_visible("input[aria-label='Use compression']:checked")
        b.wait_visible("input[aria-label='Use deduplication']:checked")

        # create a filesystem
        self.content_row_action(1, "Format")
        self.dialog({"type": "xfs",
                     "name": "vdofs",
                     "mount_point": "/run/data"})
        self.content_row_wait_in_col(1, 2, "xfs filesystem")

        # compressible data should affect logical usage
        m.execute("dd if=/dev/zero of=/run/data/empty bs=1M count=1000")
        self.content_row_wait_in_col(1, 4, "1.08 / 9.99 GiB")
        # but not physical usage
        self.content_tab_wait_in_info(1, 2, "Data used", "4.00 GiB (67%)")

        # incompressible data
        m.execute("dd if=/dev/urandom of=/run/data/gibberish bs=1M count=1000")
        self.content_row_wait_in_col(1, 4, "2.05 / 9.99 GiB")
        # equal amount of physical space (not completely predictable due to random data)
        self.content_tab_wait_in_info(1, 2, "Data used", "4.8", "4.9")
        self.content_tab_wait_in_info(1, 2, "Data used", cond=lambda sel: re.search(r"\(8[1234]%\)", b.text(sel)))

        def wait_prop(device, prop, value):
            m.execute(f"until lvdisplay --noheadings -Co {prop} /dev/vdo_vgroup/{device} | grep -q '{value}'; do sleep 0.1; done")

        # change compression/deduplication
        b.click("input[aria-label='Use compression']")
        b.wait_visible("input[aria-label='Use compression']:not(checked):not([disabled])")
        wait_prop(pool_name, "vdo_compression_state", "offline")
        b.click("input[aria-label='Use compression']")
        b.wait_visible("input[aria-label='Use compression']:checked:not([disabled])")
        wait_prop(pool_name, "vdo_compression_state", "online")

        b.click("input[aria-label='Use deduplication']")
        b.wait_visible("input[aria-label='Use deduplication']:not(checked):not([disabled])")
        wait_prop(pool_name, "vdo_index_state", "offline")
        b.click("input[aria-label='Use deduplication']")
        b.wait_visible("input[aria-label='Use deduplication']:checked:not([disabled])")
        wait_prop(pool_name, "vdo_index_state", "online")

        # grow volume
        self.content_tab_action(1, 1, "Grow")
        self.dialog({"size": 12 * 1024})
        self.content_tab_wait_in_info(1, 1, "Size", "12 GiB")
        wait_prop("vdo0", "lv_size", "12.00g")

        # grow pool
        self.content_tab_action(1, 2, "Grow")
        self.dialog({"size": 8 * 1024})
        self.content_tab_wait_in_info(1, 2, "Size", "8 GiB")
        wait_prop(pool_name, "lv_size", "8.00g")

        # deleting the vdo0 volume deletes the pool as well
        self.content_dropdown_action(1, "Delete")
        self.confirm()
        b.wait_in_text("#detail-content", "No logical volumes")
        self.assertEqual(m.execute("lvs --noheadings").strip(), "")

        # create VDO LV with customized options
        b.click("button:contains(Create new logical volume)")
        self.dialog_wait_open()
        b._wait_present("[data-field='purpose'] select option[value='block']")
        self.dialog_set_val("name", "vdo0")
        self.dialog_set_val("purpose", "vdo")
        self.dialog_set_val("vdo_psize", 6 * 1024)
        # grossly overcommitted
        self.dialog_set_val("vdo_lsize", 20 * 1024)
        self.dialog_set_val("vdo_options.compression", False)
        self.dialog_apply()
        self.dialog_wait_close()

        self.content_row_wait_in_col(1, 1, "vdo0")
        # Volume tab
        self.content_tab_wait_in_info(1, 1, "Name", "vdo0")
        self.content_tab_wait_in_info(1, 1, "Size", "20 GiB")
        # VDO Pool tab
        self.content_tab_wait_in_info(1, 2, "Size", "6 GiB")
        b.wait_visible("input[aria-label='Use compression']:not(:checked)")
        b.wait_visible("input[aria-label='Use deduplication']:checked")
        wait_prop(pool_name, "vdo_compression_state", "offline")
        wait_prop(pool_name, "vdo_index_state", "online")

        # delete again
        self.content_dropdown_action(1, "Delete")
        self.confirm()
        b.wait_in_text("#detail-content", "No logical volumes")
        self.assertEqual(m.execute("lvs --noheadings").strip(), "")

        # react to CLI
        m.execute("lvcreate --type vdo --size 6g --virtualsize 10g --name vdo1 --yes vdo_vgroup")
        self.content_row_wait_in_col(1, 1, "vdo1")
        m.execute("lvremove --yes /dev/vdo_vgroup/vdo1")
        b.wait_in_text("#detail-content", "No logical volumes")


@skipImage("no matching kmod-kvdo package yet on RHEL 9", "rhel-9-0", "rhel-9-0-distropkg")
class TestStorageLegacyVDO(StorageCase):

    def testVdo(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        if m.image not in SUPPORTED_OS:
            return

        b.wait_visible("#devices")

        # Make a logical volume for use as the backing device.
        m.add_disk("10G", serial="DISK1")
        b.wait_in_text("#drives", "DISK1")
        m.execute("vgcreate vdo_vgroup /dev/sda && lvcreate -n lvol -L 5G vdo_vgroup")
        # Create VDO; this is not supported any more, thus no UI for it
        m.execute("vdo create --device /dev/vdo_vgroup/lvol --name vdo0 --vdoLogicalSize 5G")

        b.wait_in_text("#devices", "vdo0")
        b.click("#devices .sidepanel-row:contains(vdo0)")
        b.wait_visible("#storage-detail")

        def detail(index):
            return f'#detail-header .pf-c-description-list__group:nth-of-type({index}) > dd'

        b.wait_text(detail(1), "/dev/mapper/vdo0")
        b.wait_in_text(detail(2), "vdo_vgroup")
        b.wait_in_text(detail(3), "used of 5 GiB")
        b.wait_in_text(detail(4), "used of 5 GiB")
        b.wait_text(detail(5), "256 MiB")
        b.wait_visible(detail(6) + " input:checked")
        b.wait_visible(detail(7) + " input:checked")

        # Make a filesystem on it

        self.content_row_wait_in_col(1, 2, "Unrecognized data")
        self.content_row_action(1, "Format")
        self.dialog({"type": "xfs",
                     "name": "FILESYSTEM",
                     "mount_point": "/run/data"})
        self.content_row_wait_in_col(1, 2, "xfs filesystem")
        # _netdev etc should have been prefilled
        self.content_tab_wait_in_info(1, 1, "Mount point", "_netdev")
        self.content_tab_wait_in_info(1, 1, "Mount point", "x-systemd.device-timeout=0")
        self.content_tab_wait_in_info(1, 1, "Mount point", "x-systemd.requires=vdo.service")
        self.content_row_wait_in_col(1, 4, "/ 4.99 GiB", alternate_val="/ 5.0 GiB")

        # Grow physical

        m.execute("lvresize vdo_vgroup/lvol -L 9G")
        b.wait_in_text(".pf-c-alert__description", '5 GiB of 9 GiB')
        b.click("button:contains('Grow to take all space')")
        b.wait_not_present(".pf-c-alert")
        b.wait_in_text(detail(3), "used of 9 GiB")

        # Grow logical

        b.click(detail(4) + " button:contains(Grow)")
        self.dialog({"lsize": 10 * 1024})
        b.wait_in_text(detail(4), "used of 10 GiB")
        self.content_row_wait_in_col(1, 4, "/ 9.99 GiB", alternate_val="/ 10.0 GiB")

        # Stop

        b.wait_visible('#detail-content table')
        b.click('.pf-c-card__header:contains("VDO") button:contains("Stop")')
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "unmount, stop")
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_not_present('#detail-content table')

        # Delete

        b.click('.pf-c-card__header:contains("VDO") button:contains("Delete")')
        self.dialog_wait_open()
        self.dialog_apply_with_retry(expected_errors=["Device or resource busy"])
        b.wait_visible("#storage")
        b.wait_not_in_text("#devices", "vdo0")

    def testBrokenVdo(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        if m.image not in SUPPORTED_OS:
            return

        b.wait_visible("#devices")

        m.add_disk("10G", serial="DISK1")
        b.wait_in_text("#drives", "DISK1")

        # Install a valid configuration file that describes a broken VDO
        m.write("/etc/vdoconf.yml", """
config: !Configuration
  vdos:
    vdo0: !VDOService
      _operationState: beginCreate
      ackThreads: 1
      activated: enabled
      bioRotationInterval: 64
      bioThreads: 4
      blockMapCacheSize: 128M
      blockMapPeriod: 16380
      compression: enabled
      cpuThreads: 2
      deduplication: enabled
      device: /dev/sda
      hashZoneThreads: 1
      indexCfreq: 0
      indexMemory: 0.25
      indexSparse: disabled
      indexThreads: 0
      logicalBlockSize: 4096
      logicalSize: 10G
      logicalThreads: 1
      name: vdo0
      physicalSize: 10G
      physicalThreads: 1
      readCache: disabled
      readCacheSize: 0M
      slabSize: 2G
      writePolicy: sync
  version: 538380551
""")

        b.wait_in_text("#devices", "vdo0")
        b.click("#devices .sidepanel-row:contains(vdo0)")

        b.click("#storage-detail .pf-m-danger button:contains('Remove device')")
        b.wait_visible("#storage")
        b.wait_not_in_text("#devices", "vdo0")

    def testBrokenVdoConfig(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        if m.image not in SUPPORTED_OS:
            return

        b.wait_visible("#devices")

        # Install a valid configuration file
        m.write("/etc/vdoconf.yml", """
config: !Configuration
  vdos:
    vdo0: !VDOService
      _operationState: finished
      ackThreads: 1
      activated: enabled
      bioRotationInterval: 64
      bioThreads: 4
      blockMapCacheSize: 128M
      blockMapPeriod: 16380
      compression: enabled
      cpuThreads: 2
      deduplication: enabled
      device: /dev/sda
      hashZoneThreads: 1
      indexCfreq: 0
      indexMemory: 0.25
      indexSparse: disabled
      indexThreads: 0
      logicalBlockSize: 4096
      logicalSize: 10G
      logicalThreads: 1
      name: vdo0
      physicalSize: 10G
      physicalThreads: 1
      readCache: disabled
      readCacheSize: 0M
      slabSize: 2G
      writePolicy: sync
  version: 538380551
""")

        b.wait_in_text("#devices", "vdo0")

        # Install a broken configuration file
        m.write("/etc/vdoconf.yml", """
config: !Configuration
  vdos:
    vdo0: !VDOService
      blah: 12
""")

        b.wait_not_in_text("#devices", "vdo0")

        # Install a valid configuration file again
        m.write("/etc/vdoconf.yml", """
config: !Configuration
  vdos:
    vdo1: !VDOService
      _operationState: finished
      ackThreads: 1
      activated: enabled
      bioRotationInterval: 64
      bioThreads: 4
      blockMapCacheSize: 128M
      blockMapPeriod: 16380
      compression: enabled
      cpuThreads: 2
      deduplication: enabled
      device: /dev/sda
      hashZoneThreads: 1
      indexCfreq: 0
      indexMemory: 0.25
      indexSparse: disabled
      indexThreads: 0
      logicalBlockSize: 4096
      logicalSize: 10G
      logicalThreads: 1
      name: vdo1
      physicalSize: 10G
      physicalThreads: 1
      readCache: disabled
      readCacheSize: 0M
      slabSize: 2G
      writePolicy: sync
  version: 538380551
""")

        b.wait_in_text("#devices", "vdo1")


class TestStoragePackagesVDO(PackageCase, StorageHelpers):

    def testVdoMissingPackages(self):
        m = self.machine
        b = self.browser

        if m.image not in SUPPORTED_OS:
            self.skipTest("No vdo available")

        m.execute("pkcon remove -y vdo")

        # do *not* create vdo package yet
        self.createPackage("kmod-kvdo", "999", "1")
        self.enableRepo()
        m.execute("pkcon refresh")

        self.login_and_go("/storage")
        m.add_disk("10G", serial="DISK1")
        b.wait_in_text("#drives", "DISK1")
        m.execute("vgcreate vdo_vgroup /dev/sda")
        b.wait_in_text("#devices", "vdo_vgroup")

        b.click('.sidepanel-row:contains("vdo_vgroup")')
        b.click("button:contains(Create new logical volume)")
        self.dialog_wait_open()
        b._wait_present("[data-field='purpose'] select option[value='block']")
        # no package installation helper text
        self.assertFalse(b.is_present("#dialog .pf-c-helper-text"))
        self.dialog_set_val("purpose", "vdo")
        # shows the package installation note
        b.wait_in_text("#dialog .pf-c-helper-text", "vdo package will be installed")

        # vdo package does not exist
        self.dialog_apply()
        b.wait_in_text("#dialog .pf-c-alert.pf-m-danger", "vdo is not available from any repository")

        self.createPackage("vdo", "999", "1")
        self.enableRepo()

        self.dialog_apply()
        # gets over package installation now, but it's a mock package
        b.wait_in_text("#dialog .pf-c-alert.pf-m-danger", "vdoformat")
        b.wait_in_text("#dialog .pf-c-alert.pf-m-danger", "No such file or directory")
        # but it got past package installation
        self.assertIn("999", m.execute("rpm -q vdo"))

        self.dialog_cancel()


if __name__ == '__main__':
    test_main()
